package ch.msoftch.gui.utils;

import java.util.*;
import org.eclipse.jdt.core.*;
import org.eclipse.jdt.core.dom.*;
import org.eclipse.jdt.core.dom.Modifier.*;
import org.eclipse.jface.text.*;
import org.eclipse.text.edits.*;

/**
 * Helper object for building/editing a class with AST.
 * 
 * @author hautle
 */
public class ClassBuilder {
	/** The AST Tree. */
	protected AST ast;

	/** The compilation unit hold by the {@link #ast}. */
	protected CompilationUnit cu;

	/** The sole type declared in the {@link #cu}. */
	protected TypeDeclaration type;

	/** The "real" compilation unit. */
	protected ICompilationUnit unit;

	/**
	 * Default constructor.
	 */
	public ClassBuilder() {
	}

	/**
	 * Default constructor.
	 * 
	 * @param unit The compilation unit to use
	 */
	public ClassBuilder(ICompilationUnit unit) {
		setUp(unit);
	}

	/**
	 * Initializes the class builder.
	 * 
	 * @param unit The compilation unit to use
	 */
	public void setUp(ICompilationUnit unit) {
		if (ast != null)
			return;
		this.unit = unit;
		// we use the parser only to get a AST which is (re)writable
		final ASTParser parser = ASTParser.newParser(AST.JLS3);
		parser.setSource(unit);
		cu = (CompilationUnit) parser.createAST(null);
		cu.recordModifications();
		ast = cu.getAST();
		setMainType();
	}

	/**
	 * Set the main type from the compilation unit.
	 */
	@SuppressWarnings("unchecked")
	private void setMainType() {
		String name = unit.getElementName();
		name = name.substring(0, name.length() - 5);
		for (TypeDeclaration d : (List<TypeDeclaration>) cu.types()) {
			if (!name.equals(d.getName().getIdentifier()))
				continue;
			type = d;
			break;
		}
	}

	/**
	 * Creates and sets the main type.
	 * 
	 * @param modifier The modifier of the class
	 * @param packageName The package name
	 * @param className The class name
	 */
	@SuppressWarnings("unchecked")
	public void createMainType(ModifierKeyword modifier, String packageName, String className) {
		if (type != null)
			type.delete();
		// set the package of the compilation unit
		final PackageDeclaration pack = ast.newPackageDeclaration();
		pack.setName(ast.newName(packageName.split("\\.")));
		cu.setPackage(pack);
		// define the sole type of the unit
		type = ast.newTypeDeclaration();
		type.setName(ast.newSimpleName(className));
		setModifiers(type.modifiers(), modifier);
		cu.types().add(type);
	}

	/**
	 * Adds the given method.
	 * 
	 * @param m The method
	 */
	@SuppressWarnings("unchecked")
	public void addMethod(MethodDeclaration m) {
		type.bodyDeclarations().add(m);
	}

	/**
	 * Adds the given field.
	 * 
	 * @param f The field
	 */
	@SuppressWarnings("unchecked")
	public void addField(FieldDeclaration f) {
		type.bodyDeclarations().add(f);
	}

	/**
	 * Adds the given field at the beginning of the class.
	 * 
	 * @param f The field
	 */
	@SuppressWarnings("unchecked")
	public void addFieldFirst(FieldDeclaration f) {
		type.bodyDeclarations().add(0, f);
	}

	/**
	 * Adds the given import statement.
	 * 
	 * @param importStmt The import statement
	 */
	@SuppressWarnings("unchecked")
	public void addImport(String importStmt) {
		final ImportDeclaration id = ast.newImportDeclaration();
		final int len = importStmt.length();
		if (importStmt.charAt(len - 1) == '*') {
			id.setOnDemand(true);
			importStmt = importStmt.substring(0, len - 2);
		}
		id.setName(ast.newName(importStmt.split("\\.")));
		cu.imports().add(id);
	}

	/**
	 * Appends the given modifiers to the passed modifier list.
	 * 
	 * @param modList The modifier list to extend
	 * @param modifiers The modifiers to append
	 */
	@SuppressWarnings("unchecked")
	public void setModifiers(List modList, ModifierKeyword... modifiers) {
		for (ModifierKeyword m : modifiers)
			modList.add(ast.newModifier(m));
	}

	/**
	 * Creates a textelement containing the given text.
	 * 
	 * @param txt The text
	 * @return A textelement
	 */
	public TextElement createText(String txt) {
		final TextElement el = ast.newTextElement();
		el.setText(txt);
		return el;
	}

	/**
	 * Creates a block holding the given statements.
	 * 
	 * @param stmts The statements to add to the block
	 * @return The block
	 */
	@SuppressWarnings("unchecked")
	public Block createBlock(Expression... stmts) {
		final Block b = ast.newBlock();
		final List content = b.statements();
		for (Expression s : stmts)
			content.add(ast.newExpressionStatement(s));
		return b;
	}

	/**
	 * Creates a block holding the given statements.
	 * 
	 * @param stmts The statements to add to the block
	 * @return The block
	 */
	@SuppressWarnings("unchecked")
	public Block createBlock(Statement... stmts) {
		final Block b = ast.newBlock();
		final List content = b.statements();
		for (Statement s : stmts)
			content.add(s);
		return b;
	}

	/**
	 * Creates an "primitive-operation" expression.
	 * 
	 * @param left The left expression
	 * @param op The operator
	 * @param right The right expression
	 * @return The expression
	 */
	public InfixExpression createInfixExpression(Expression left, InfixExpression.Operator op, Expression right) {
		final InfixExpression e = ast.newInfixExpression();
		e.setOperator(op);
		e.setRightOperand(right);
		e.setLeftOperand(left);
		return e;
	}

	/**
	 * Creates a field access for the given field.
	 * 
	 * @param name The field name
	 * @return The field access
	 */
	public FieldAccess createFieldAccess(String name) {
		final FieldAccess fa = ast.newFieldAccess();
		fa.setName(ast.newSimpleName(name));
		return fa;
	}

	/**
	 * Creates a return statement.
	 * 
	 * @param exp The expression of the return statement
	 * @return The return statement
	 */
	public ReturnStatement createReturn(Expression exp) {
		final ReturnStatement r = ast.newReturnStatement();
		r.setExpression(exp);
		return r;
	}

	/**
	 * Creates a simple try catch statement.
	 * 
	 * @param tryBlock The try block
	 * @param catchBlock The catch block
	 * @param exceptionType The exception type of the catch block
	 * @param exceptionVar The name of the exception variable in the catch block
	 * @return The statement
	 */
	@SuppressWarnings("unchecked")
	public TryStatement createTryCatchBlock(Block tryBlock, Block catchBlock, String exceptionType, String exceptionVar) {
		final TryStatement tryB = ast.newTryStatement();
		tryB.setBody(tryBlock);
		final CatchClause catchB = ast.newCatchClause();
		catchB.setException(createLocalVar(exceptionType, exceptionVar));
		catchB.setBody(catchBlock);
		tryB.catchClauses().add(catchB);
		return tryB;
	}

	/**
	 * Creates a field.
	 * 
	 * @param type The field type
	 * @param name The field name
	 * @param initializer The field initializer expression or null
	 * @param modifiers The modifiers of the field
	 * @return The created field object
	 */
	public FieldDeclaration createField(String type, String name, Expression initializer, ModifierKeyword... modifiers) {
		final VariableDeclarationFragment vars = ast.newVariableDeclarationFragment();
		vars.setName(ast.newSimpleName(name));
		vars.setInitializer(initializer);
		final FieldDeclaration var = ast.newFieldDeclaration(vars);
		var.setType(ast.newSimpleType(ast.newName(type)));
		setModifiers(var.modifiers(), modifiers);
		return var;
	}

	/**
	 * Returns the {@link VariableDeclarationFragment} of a {@link FieldDeclaration} if there is only one defined.
	 * 
	 * @param field The field
	 * @return The variable declaration or null
	 */
	@SuppressWarnings("unchecked")
	public VariableDeclarationFragment getVariableDeclaration(FieldDeclaration field) {
		final List frags = field.fragments();
		if (frags.size() != 1)
			return null;
		return (VariableDeclarationFragment) frags.get(0);
	}

	/**
	 * Creates a method declaration.
	 * 
	 * @param name The method name
	 * @param returnType The return type or null
	 * @param body The method body
	 * @param args The method arguments
	 * @return The method declaration
	 */
	@SuppressWarnings("unchecked")
	public MethodDeclaration createMethod(String name, String returnType, Block body, String... args) {
		final MethodDeclaration m = ast.newMethodDeclaration();
		m.setName(ast.newSimpleName(name));
		if (returnType != null)
			m.setReturnType2(ast.newSimpleType(ast.newName(returnType)));
		m.setBody(body);
		// set the method parameters
		final List params = m.parameters();
		for (String p : args) {
			final int mid = p.indexOf(' ');
			final String type = p.substring(0, mid);
			final String pName = p.substring(mid + 1);
			params.add(createLocalVar(type, pName));
		}
		return m;
	}

	/**
	 * Creates a local variable declaration.
	 * 
	 * @param type The type
	 * @param name The name
	 * @return The variable declaration
	 */
	public SingleVariableDeclaration createLocalVar(String type, String name) {
		final SingleVariableDeclaration v = ast.newSingleVariableDeclaration();
		v.setName(ast.newSimpleName(name));
		v.setType(ast.newSimpleType(ast.newName(type)));
		return v;
	}

	/**
	 * Creates a string literal with the given content.
	 * 
	 * @param value The string
	 * @return The created string literal
	 */
	public StringLiteral createString(String value) {
		final StringLiteral str = ast.newStringLiteral();
		str.setLiteralValue(value);
		return str;
	}

	/**
	 * Creates a method invocation object.
	 * 
	 * @param base The expression on which the method should be invoked or null if it's just a simple method call
	 * @param method The method name
	 * @param args The method arguments
	 * @return The invocation object
	 */
	@SuppressWarnings("unchecked")
	public MethodInvocation createInvocation(Expression base, String method, Expression... args) {
		final MethodInvocation mv = ast.newMethodInvocation();
		mv.setName(ast.newSimpleName(method));
		mv.setExpression(base);
		// set arguments
		final List mArgs = mv.arguments();
		for (Expression e : args)
			mArgs.add(e);
		return mv;
	}

	/**
	 * Creates a {@link Javadoc} node with the given text.
	 * 
	 * @param text The text content
	 * @return A javadoc node representing the given text
	 */
	@SuppressWarnings("unchecked")
	protected Javadoc createJavaDoc(String text) {
		final TextElement txt = ast.newTextElement();
		txt.setText(text);
		final TagElement tag = ast.newTagElement();
		tag.fragments().add(txt);
		final Javadoc doc = ast.newJavadoc();
		doc.tags().add(tag);
		return doc;
	}

	/**
	 * Returns the name of a {@link SimpleType}.
	 * 
	 * @param type The type
	 * @return The full qualified name or null
	 */
	public String getSimpleTypeName(Type type) {
		if (type.isSimpleType())
			return ((SimpleType) type).getName().getFullyQualifiedName();
		return null;
	}

	/**
	 * Write the class.
	 * 
	 * @throws JavaModelException
	 */
	public void write() throws JavaModelException {
		final TextEdit edit = cu.rewrite(new Document(unit.getSource()), null);
		unit.applyTextEdit(edit, null);
		unit.save(null, true);
	}
}
